/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
using System;
using java = biz.ritter.javapi;
using org.apache.commons.compress.archivers;

namespace org.apache.commons.compress.archivers.cpio{

    /**
     * A cpio archive consists of a sequence of files. There are several types of
     * headers defided in two categories of new and old format. The headers are
     * recognized by magic numbers:
     * 
     * <ul>
     * <li>"070701" ASCII for new portable format</li>
     * <li>"070702" ASCII for new portable format with CRC format</li>
     * <li>"070707" ASCII for old ascii (also known as Portable ASCII, odc or old
     * character format</li>
     * <li>070707 binary for old binary</li>
     * </ul>
     *
     * <p>The old binary format is limited to 16 bits for user id, group
     * id, device, and inode numbers. It is limited to 4 gigabyte file
     * sizes.
     * 
     * The old ASCII format is limited to 18 bits for the user id, group
     * id, device, and inode numbers. It is limited to 8 gigabyte file
     * sizes.
     * 
     * The new ASCII format is limited to 4 gigabyte file sizes.
     * 
     * CPIO 2.5 knows also about tar, but it is not recognized here.</p>
     * 
     * 
     * <h3>OLD FORMAT</h3>
     * 
     * <p>Each file has a 76 (ascii) / 26 (binary) byte header, a variable
     * length, NUL terminated filename, and variable length file data. A
     * header for a filename "TRAILER!!!" indicates the end of the
     * archive.</p>
     * 
     * <p>All the fields in the header are ISO 646 (approximately ASCII)
     * strings of octal numbers, left padded, not NUL terminated.</p>
     * 
     * <pre>
     * FIELDNAME        NOTES 
     * c_magic          The integer value octal 070707.  This value can be used to deter-
     *                  mine whether this archive is written with little-endian or big-
     *                  endian integers.
     * c_dev            Device that contains a directory entry for this file 
     * c_ino            I-node number that identifies the input file to the file system 
     * c_mode           The mode specifies both the regular permissions and the file type.
     * c_uid            Numeric User ID of the owner of the input file 
     * c_gid            Numeric Group ID of the owner of the input file 
     * c_nlink          Number of links that are connected to the input file 
     * c_rdev           For block special and character special entries, this field 
     *                  contains the associated device number.  For all other entry types,
     *                  it should be set to zero by writers and ignored by readers.
     * c_mtime[2]       Modification time of the file, indicated as the number of seconds
     *                  since the start of the epoch, 00:00:00 UTC January 1, 1970.  The
     *                  four-byte integer is stored with the most-significant 16 bits
     *                  first followed by the least-significant 16 bits.  Each of the two
     *                  16 bit values are stored in machine-native byte order.
     * c_namesize       Length of the path name, including the terminating null byte 
     * c_filesize[2]    Length of the file in bytes. This is the length of the data 
     *                  section that follows the header structure. Must be 0 for 
     *                  FIFOs and directories
     *
     * All fields are unsigned short fields with 16-bit integer values
     * apart from c_mtime and c_filesize which are 32-bit integer values
     * </pre>
     * 
     * <p>If necessary, the filename and file data are padded with a NUL byte to an even length</p>
     * 
     * <p>Special files, directories, and the trailer are recorded with
     * the h_filesize field equal to 0.</p>
     * 
     * <p>In the ASCII version of this format, the 16-bit entries are represented as 6-byte octal numbers,
     * and the 32-bit entries are represented as 11-byte octal numbers. No padding is added.</p>
     * 
     * <h3>NEW FORMAT</h3>
     * 
     * <p>Each file has a 110 byte header, a variable length, NUL
     * terminated filename, and variable length file data. A header for a
     * filename "TRAILER!!!" indicates the end of the archive. All the
     * fields in the header are ISO 646 (approximately ASCII) strings of
     * hexadecimal numbers, left padded, not NUL terminated.</p>
     * 
     * <pre>
     * FIELDNAME        NOTES 
     * c_magic[6]       The string 070701 for new ASCII, the string 070702 for new ASCII with CRC
     * c_ino[8]
     * c_mode[8]
     * c_uid[8]
     * c_gid[8]
     * c_nlink[8]
     * c_mtim[8]
     * c_filesize[8]    must be 0 for FIFOs and directories 
     * c_maj[8]
     * c_min[8] 
     * c_rmaj[8]        only valid for chr and blk special files 
     * c_rmin[8]        only valid for chr and blk special files 
     * c_namesize[8]    count includes terminating NUL in pathname 
     * c_check[8]       0 for "new" portable format; for CRC format
     *                  the sum of all the bytes in the file
     * </pre>
     * 
     * <p>New ASCII Format The "new" ASCII format uses 8-byte hexadecimal
     * fields for all numbers and separates device numbers into separate
     * fields for major and minor numbers.</p>
     * 
     * <p>The pathname is followed by NUL bytes so that the total size of
     * the fixed header plus pathname is a multiple of four. Likewise, the
     * file data is padded to a multiple of four bytes.</p>
     * 
     * <p>This class uses mutable fields and is not considered to be
     * threadsafe.</p>
     * 
     * <p>Based on code from the jRPM project (http://jrpm.sourceforge.net).</p>
     *
     * <p>The MAGIC numbers and other constants are defined in {@link CpioConstants}</p>
     * 
     * <p>
     * N.B. does not handle the cpio "tar" format
     * </p>
     * @NotThreadSafe
     * @see "http://people.freebsd.org/~kientzle/libarchive/man/cpio.5.txt"
     */
    public class CpioArchiveEntry : CpioConstants, ArchiveEntry {

        // Header description fields - should be same throughout an archive

        /**
         * See constructor documenation for possible values.
         */
        private short fileFormat; 

        /** The number of bytes in each header record; depends on the file format */
        private int headerSize;

        /** The boundary to which the header and data elements are aligned: 0, 2 or 4 bytes */
        private int alignmentBoundary;

        // Header fields

        private long chksum = 0;

        /** Number of bytes in the file */
        private long filesize = 0;

        private long gid = 0;

        private long inode = 0;

        private long maj = 0;

        private long min = 0;

        private long mode = 0;

        private long mtime = 0;

        private String name;

        private long nlink = 0;

        private long rmaj = 0;

        private long rmin = 0;

        private long uid = 0;

        /**
         * Creates a CPIOArchiveEntry with a specified format.
         * 
         * @param format
         *            The cpio format for this entry.
         * <br/>
         * Possible format values are:
         * <p>
         * CpioConstants.FORMAT_NEW<br/>
         * CpioConstants.FORMAT_NEW_CRC<br/>
         * CpioConstants.FORMAT_OLD_BINARY<br/>
         * CpioConstants.FORMAT_OLD_ASCII<br/>
         * 
         */
        public CpioArchiveEntry(short format) {
            this.setFileFormat (format);
        }

        /**
         * Creates a CPIOArchiveEntry with a specified name. The format of
         * this entry will be the new format.
         * 
         * @param name
         *            The name of this entry.
         */
        public CpioArchiveEntry(String newName) {
            this.setFileFormat(CpioConstants.FORMAT_NEW);
            this.name = newName;
        }

        protected void setFileFormat (short format) {
            switch (format) {
            case CpioConstants.FORMAT_NEW:
                this.headerSize = 110;
                this.alignmentBoundary = 4;
                break;
            case CpioConstants.FORMAT_NEW_CRC:
                this.headerSize = 110;
                this.alignmentBoundary = 4;
                break;
            case CpioConstants.FORMAT_OLD_ASCII:
                this.headerSize = 76;
                this.alignmentBoundary = 0;
                break;
            case CpioConstants.FORMAT_OLD_BINARY:
                this.headerSize = 26;
                this.alignmentBoundary = 2;
                break;
            default:
                throw new java.lang.IllegalArgumentException("Unknown header type");
            }
            this.fileFormat = format;
        }

        /**
         * Creates a CPIOArchiveEntry with a specified name.
         * 
         * @param format
         *            The cpio format for this entry.
         * @param name
         *            The name of this entry.
         * <br/>
         * Possible format values are:
         * <p>
         * CpioConstants.FORMAT_NEW<br/>
         * CpioConstants.FORMAT_NEW_CRC<br/>
         * CpioConstants.FORMAT_OLD_BINARY<br/>
         * CpioConstants.FORMAT_OLD_ASCII<br/>
         * 
         * @since Apache Commons Compress 1.1
         */
        public CpioArchiveEntry(short format, String newName) {
            this.setFileFormat(format);
            this.name = newName;
        }

        /**
         * Creates a CPIOArchiveEntry with a specified name. The format of
         * this entry will be the new format.
         * 
         * @param name
         *            The name of this entry.
         * @param size
         *            The size of this entry
         */
        public CpioArchiveEntry(String newName, long newSize) {
            this.setFileFormat(CpioConstants.FORMAT_NEW);
            this.name = newName;
            this.setSize(newSize);
        }

        /**
         * Creates a CPIOArchiveEntry with a specified name.
         * 
         * @param format
         *            The cpio format for this entry.
         * @param name
         *            The name of this entry.
         * @param size
         *            The size of this entry
         * <br/>
         * Possible format values are:
         * <p>
         * CpioConstants.FORMAT_NEW<br/>
         * CpioConstants.FORMAT_NEW_CRC<br/>
         * CpioConstants.FORMAT_OLD_BINARY<br/>
         * CpioConstants.FORMAT_OLD_ASCII<br/>
         * 
         * @since Apache Commons Compress 1.1
         */
        public CpioArchiveEntry(short format, String newName, long newSize) {
            this.setFileFormat(CpioConstants.FORMAT_NEW);
            this.name = newName;
            this.setSize(newSize);
        }

        /**
         * Creates a CPIOArchiveEntry with a specified name for a
         * specified file. The format of this entry will be the new
         * format.
         * 
         * @param inputFile
         *            The file to gather information from.
         * @param entryName
         *            The name of this entry.
         */
        public CpioArchiveEntry(java.io.File inputFile, String entryName) {
            this.setFileFormat(CpioConstants.FORMAT_NEW);
            this.name = entryName;
            this.setSize(inputFile.isFile() ? inputFile.length() : 0);
            long mode=0;
            if (inputFile.isDirectory()){
                mode |= CpioConstants.C_ISDIR;
            } else if (inputFile.isFile()){
                mode |= CpioConstants.C_ISREG;
            } else {
                throw new java.lang.IllegalArgumentException("Cannot determine type of file "
                                                   + inputFile.getName());
            }
            // TODO set other fields as needed
            setMode(mode);
            setTime(inputFile.lastModified() / 1000);
        }

        /**
         * Creates a CPIOArchiveEntry with a specified name for a
         * specified file.
         * 
         * @param format
         *            The cpio format for this entry.
         * @param inputFile
         *            The file to gather information from.
         * @param entryName
         *            The name of this entry.
         * <br/>
         * Possible format values are:
         * <p>
         * CpioConstants.FORMAT_NEW<br/>
         * CpioConstants.FORMAT_NEW_CRC<br/>
         * CpioConstants.FORMAT_OLD_BINARY<br/>
         * CpioConstants.FORMAT_OLD_ASCII<br/>
         * 
         * @since Apache Commons Compress 1.1
         */
        public CpioArchiveEntry(short format, java.io.File inputFile, String entryName) {
            this.setFileFormat(format);
            this.name = entryName;
            this.setSize(inputFile.isFile() ? inputFile.length() : 0);
            long mode=0;
            if (inputFile.isDirectory()){
                mode |= CpioConstants.C_ISDIR;
            } else if (inputFile.isFile()){
                mode |= CpioConstants.C_ISREG;
            } else {
                throw new java.lang.IllegalArgumentException("Cannot determine type of file "
                                                   + inputFile.getName());
            }
            // TODO set other fields as needed
            setMode(mode);
            setTime(inputFile.lastModified() / 1000);
        }

        /**
         * Check if the method is allowed for the defined format.
         */
        private void checkNewFormat() {
            if ((this.fileFormat & CpioConstants.FORMAT_NEW_MASK) == 0) {
                throw new java.lang.UnsupportedOperationException();
            }
        }

        /**
         * Check if the method is allowed for the defined format.
         */
        private void checkOldFormat() {
            if ((this.fileFormat & CpioConstants.FORMAT_OLD_MASK) == 0) {
                throw new java.lang.UnsupportedOperationException();
            }
        }

        /**
         * Get the checksum.
         * Only supported for the new formats.
         * 
         * @return Returns the checksum.
         * @throws UnsupportedOperationException if the format is not a new format
         */
        public long getChksum() {
            checkNewFormat();
            return this.chksum;
        }

        /**
         * Get the device id.
         * 
         * @return Returns the device id.
         * @throws UnsupportedOperationException
         *             if this method is called for a CPIOArchiveEntry with a new
         *             format.
         */
        public long getDevice() {
            checkOldFormat();
            return this.min;
        }

        /**
         * Get the major device id.
         * 
         * @return Returns the major device id.
         * @throws UnsupportedOperationException
         *             if this method is called for a CPIOArchiveEntry with an old
         *             format.
         */
        public long getDeviceMaj() {
            checkNewFormat();
            return this.maj;
        }

        /**
         * Get the minor device id
         * 
         * @return Returns the minor device id.
         * @throws UnsupportedOperationException if format is not a new format
         */
        public long getDeviceMin() {
            checkNewFormat();
            return this.min;
        }

        /**
         * Get the filesize.
         * 
         * @return Returns the filesize.
         * @see org.apache.commons.compress.archivers.ArchiveEntry#getSize()
         */
        public long getSize() {
            return this.filesize;
        }

        /**
         * Get the format for this entry.
         * 
         * @return Returns the format.
         */
        public short getFormat() {
            return this.fileFormat;
        }

        /**
         * Get the group id.
         * 
         * @return Returns the group id.
         */
        public long getGID() {
            return this.gid;
        }

        /**
         * Get the header size for this CPIO format
         * 
         * @return Returns the header size in bytes.
         */
        public int getHeaderSize() {
            return this.headerSize;
        }

        /**
         * Get the alignment boundary for this CPIO format
         * 
         * @return Returns the aligment boundary (0, 2, 4) in bytes
         */
        public int getAlignmentBoundary() {
            return this.alignmentBoundary;
        }

        /**
         * Get the number of bytes needed to pad the header to the alignment boundary.
         * 
         * @return the number of bytes needed to pad the header (0,1,2,3)
         */
        public int getHeaderPadCount(){
            if (this.alignmentBoundary == 0) return 0;
            int size = this.headerSize+this.name.length()+1; // Name has terminating null
            int remain = size % this.alignmentBoundary;
            if (remain > 0){
                return this.alignmentBoundary - remain;
            }
            return 0;
        }

        /**
         * Get the number of bytes needed to pad the data to the alignment boundary.
         * 
         * @return the number of bytes needed to pad the data (0,1,2,3)
         */
        public int getDataPadCount(){
            if (this.alignmentBoundary == 0) return 0;
            long size = this.filesize;
            int remain = (int) (size % this.alignmentBoundary);
            if (remain > 0){
                return this.alignmentBoundary - remain;
            }
            return 0;
        }

        /**
         * Set the inode.
         * 
         * @return Returns the inode.
         */
        public long getInode() {
            return this.inode;
        }

        /**
         * Get the mode of this entry (e.g. directory, regular file).
         * 
         * @return Returns the mode.
         */
        public long getMode() {
            return mode == 0 && !CpioConstants.CPIO_TRAILER.equals(name) ? CpioConstants.C_ISREG : mode;
        }

        /**
         * Get the name.
         * 
         * @return Returns the name.
         */
        public String getName() {
            return this.name;
        }

        /**
         * Get the number of links.
         * 
         * @return Returns the number of links.
         */
        public long getNumberOfLinks() {
            return nlink == 0 ?
                (isDirectory() ? 2 : 1)
                : nlink;
        }

        /**
         * Get the remote device id.
         * 
         * @return Returns the remote device id.
         * @throws UnsupportedOperationException
         *             if this method is called for a CPIOArchiveEntry with a new
         *             format.
         */
        public long getRemoteDevice() {
            checkOldFormat();
            return this.rmin;
        }

        /**
         * Get the remote major device id.
         * 
         * @return Returns the remote major device id.
         * @throws UnsupportedOperationException
         *             if this method is called for a CPIOArchiveEntry with an old
         *             format.
         */
        public long getRemoteDeviceMaj() {
            checkNewFormat();
            return this.rmaj;
        }

        /**
         * Get the remote minor device id.
         * 
         * @return Returns the remote minor device id.
         * @throws UnsupportedOperationException
         *             if this method is called for a CPIOArchiveEntry with an old
         *             format.
         */
        public long getRemoteDeviceMin() {
            checkNewFormat();
            return this.rmin;
        }

        /**
         * Get the time in seconds.
         * 
         * @return Returns the time.
         */
        public long getTime() {
            return this.mtime;
        }

        /** {@inheritDoc} */
        public java.util.Date getLastModifiedDate() {
            return new java.util.Date(1000 * getTime());
        }

        /**
         * Get the user id.
         * 
         * @return Returns the user id.
         */
        public long getUID() {
            return this.uid;
        }

        /**
         * Check if this entry represents a block device.
         * 
         * @return TRUE if this entry is a block device.
         */
        public bool isBlockDevice() {
            return (this.mode & CpioConstants.S_IFMT) == CpioConstants.C_ISBLK;
        }

        /**
         * Check if this entry represents a character device.
         * 
         * @return TRUE if this entry is a character device.
         */
        public bool isCharacterDevice() {
            return (this.mode & CpioConstants.S_IFMT) == CpioConstants.C_ISCHR;
        }

        /**
         * Check if this entry represents a directory.
         * 
         * @return TRUE if this entry is a directory.
         */
        public bool isDirectory() {
            return (this.mode & CpioConstants.S_IFMT) == CpioConstants.C_ISDIR;
        }

        /**
         * Check if this entry represents a network device.
         * 
         * @return TRUE if this entry is a network device.
         */
        public bool isNetwork() {
            return (this.mode & CpioConstants.S_IFMT) == CpioConstants.C_ISNWK;
        }

        /**
         * Check if this entry represents a pipe.
         * 
         * @return TRUE if this entry is a pipe.
         */
        public bool isPipe() {
            return (this.mode & CpioConstants.S_IFMT) == CpioConstants.C_ISFIFO;
        }

        /**
         * Check if this entry represents a regular file.
         * 
         * @return TRUE if this entry is a regular file.
         */
        public bool isRegularFile() {
            return (this.mode & CpioConstants.S_IFMT) == CpioConstants.C_ISREG;
        }

        /**
         * Check if this entry represents a socket.
         * 
         * @return TRUE if this entry is a socket.
         */
        public bool isSocket() {
            return (this.mode & CpioConstants.S_IFMT) == CpioConstants.C_ISSOCK;
        }

        /**
         * Check if this entry represents a symbolic link.
         * 
         * @return TRUE if this entry is a symbolic link.
         */
        public bool isSymbolicLink() {
            return (this.mode & CpioConstants.S_IFMT) == CpioConstants.C_ISLNK;
        }

        /**
         * Set the checksum. The checksum is calculated by adding all bytes of a
         * file to transfer (crc += buf[pos] & 0xFF).
         * 
         * @param chksum
         *            The checksum to set.
         */
        public void setChksum(long newChksum) {
            checkNewFormat();
            this.chksum = newChksum;
        }

        /**
         * Set the device id.
         * 
         * @param device
         *            The device id to set.
         * @throws UnsupportedOperationException
         *             if this method is called for a CPIOArchiveEntry with a new
         *             format.
         */
        public void setDevice(long device) {
            checkOldFormat();
            this.min = device;
        }

        /**
         * Set major device id.
         * 
         * @param maj
         *            The major device id to set.
         */
        public void setDeviceMaj(long maj) {
            checkNewFormat();
            this.maj = maj;
        }

        /**
         * Set the minor device id
         * 
         * @param min
         *            The minor device id to set.
         */
        public void setDeviceMin(long min) {
            checkNewFormat();
            this.min = min;
        }

        /**
         * Set the filesize.
         * 
         * @param size
         *            The filesize to set.
         */
        public void setSize(long size) {
            if (size < 0 || size > 0xFFFFFFFFL) {
                throw new java.lang.IllegalArgumentException("invalid entry size <" + size
                                                   + ">");
            }
            this.filesize = size;
        }

        /**
         * Set the group id.
         * 
         * @param gid
         *            The group id to set.
         */
        public void setGID(long gid) {
            this.gid = gid;
        }

        /**
         * Set the inode.
         * 
         * @param inode
         *            The inode to set.
         */
        public void setInode(long inode) {
            this.inode = inode;
        }

        /**
         * Set the mode of this entry (e.g. directory, regular file).
         * 
         * @param mode
         *            The mode to set.
         */
        public void setMode(long mode) {
            long maskedMode = mode & CpioConstants.S_IFMT;
            switch ((int) maskedMode) {
            case CpioConstants.C_ISDIR:
            case CpioConstants.C_ISLNK:
            case CpioConstants.C_ISREG:
            case CpioConstants.C_ISFIFO:
            case CpioConstants.C_ISCHR:
            case CpioConstants.C_ISBLK:
            case CpioConstants.C_ISSOCK:
            case CpioConstants.C_ISNWK:
                break;
            default:
                throw new java.lang.IllegalArgumentException(
                                                   "Unknown mode. "
                                                   + "Full: " + java.lang.Long.toHexString(mode) 
                                                   + " Masked: " + java.lang.Long.toHexString(maskedMode));
            }

            this.mode = mode;
        }

        /**
         * Set the name.
         * 
         * @param name
         *            The name to set.
         */
        public void setName(String name) {
            this.name = name;
        }

        /**
         * Set the number of links.
         * 
         * @param nlink
         *            The number of links to set.
         */
        public void setNumberOfLinks(long nlink) {
            this.nlink = nlink;
        }

        /**
         * Set the remote device id.
         * 
         * @param device
         *            The remote device id to set.
         * @throws UnsupportedOperationException
         *             if this method is called for a CPIOArchiveEntry with a new
         *             format.
         */
        public void setRemoteDevice(long device) {
            checkOldFormat();
            this.rmin = device;
        }

        /**
         * Set the remote major device id.
         * 
         * @param rmaj
         *            The remote major device id to set.
         * @throws UnsupportedOperationException
         *             if this method is called for a CPIOArchiveEntry with an old
         *             format.
         */
        public void setRemoteDeviceMaj(long rmaj) {
            checkNewFormat();
            this.rmaj = rmaj;
        }

        /**
         * Set the remote minor device id.
         * 
         * @param rmin
         *            The remote minor device id to set.
         * @throws UnsupportedOperationException
         *             if this method is called for a CPIOArchiveEntry with an old
         *             format.
         */
        public void setRemoteDeviceMin(long rmin) {
            checkNewFormat();
            this.rmin = rmin;
        }

        /**
         * Set the time in seconds.
         * 
         * @param time
         *            The time to set.
         */
        public void setTime(long time) {
            this.mtime = time;
        }

        /**
         * Set the user id.
         * 
         * @param uid
         *            The user id to set.
         */
        public void setUID(long uid) {
            this.uid = uid;
        }

        /* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        public override int GetHashCode() {
            int prime = 31;
            int result = 1;
            result = prime * result + ((name == null) ? 0 : name.GetHashCode());
            return result;
        }

        /* (non-Javadoc)
         * @see java.lang.Object#equals(java.lang.Object)
         */
        public override bool Equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null || this.GetType() != obj.GetType()) {
                return false;
            }
            CpioArchiveEntry other = (CpioArchiveEntry) obj;
            if (name == null) {
                if (other.name != null) {
                    return false;
                }
            } else if (!name.equals(other.name)) {
                return false;
            }
            return true;
        }
    }
}